package RayTracing;


import java.awt.Transparency;
import java.awt.color.*;
import java.awt.image.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


import javax.imageio.ImageIO;

/**
 *  Main class for ray tracing exercise.
 */
public class RayTracer {

	public int imageWidth;
	public int imageHeight;
	
	private Camera cam = new Camera();
	private Settings set = new Settings();

	public ArrayList<Material> mtl = new ArrayList<Material>();
	public ArrayList<Sphere> sph = new ArrayList<Sphere>();
	public ArrayList<Plane> pln = new ArrayList<Plane>();
	public ArrayList<Ellipsoid> elp = new ArrayList<Ellipsoid>();
	public ArrayList<Lights> lgt = new ArrayList<Lights>();
	
	public ArrayList<Shape> shpList = new ArrayList<Shape>();
	


	public final static boolean SHOW_LOG = false;

	/**
	 * Runs the ray tracer. Takes scene file, output image file and image size as input.
	 */
	public static void main(String[] args) {

		try {

			RayTracer tracer = new RayTracer();

                        // Default values:
			tracer.imageWidth = 500;
			tracer.imageHeight = 500;

			if (args.length < 2)
				throw new RayTracerException("Not enough arguments provided. " +
						"Please specify an input scene file and an output image " +
						"file for rendering.");

			String sceneFileName = args[0];
			String outputFileName = args[1];

			if (args.length > 3)
			{
				tracer.imageWidth = Integer.parseInt(args[2]);
				tracer.imageHeight = Integer.parseInt(args[3]);
			}


			// Parse scene file:
			tracer.parseScene(sceneFileName);

			// Render scene:
			tracer.renderScene(outputFileName);

//		} catch (IOException e) {
//			System.out.println(e.getMessage());
		} catch (RayTracerException e) {
			System.out.println(e.getMessage());
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}


	}

	/**
	 * Parses the scene file and creates the scene. 
	 * Change this function so it generates the required objects.
	 */
	public void parseScene(String sceneFileName) throws IOException, RayTracerException
	{
		FileReader fr = new FileReader(sceneFileName);

		BufferedReader r = new BufferedReader(fr);
		String line = null;
		int lineNum = 0;
		System.out.println("Started parsing scene file " + sceneFileName);



		while ((line = r.readLine()) != null)
		{
			line = line.trim();
			++lineNum;

			if (line.isEmpty() || (line.charAt(0) == '#'))
			{  // This line in the scene file is a comment
				continue;
			}
			else
			{
				String code = line.substring(0, 3).toLowerCase();
				// Split according to white space characters:
				String[] params = line.substring(3).trim().toLowerCase().split("\\s+");

				if (code.equals("cam"))
				{
					int[] ImageDimesion = {imageWidth, imageHeight};
					cam = new Camera(params, ImageDimesion);

					System.out.println(String.format("Parsed camera parameters (line %d)", lineNum));
				}
				else if (code.equals("set"))
				{
					set = new Settings(params);

					System.out.println(String.format("Parsed general settings (line %d)", lineNum));
				}
				else if (code.equals("mtl"))
				{
                    mtl.add(new Material(params));

					System.out.println(String.format("Parsed material (line %d)", lineNum));
				}
				else if (code.equals("sph"))
				{
                    //sph.add(new Sphere(params));
                    shpList.add(new Sphere(params));


					System.out.println(String.format("Parsed sphere (line %d)", lineNum));
				}
				else if (code.equals("pln"))
				{
                    //pln.add(new Plane(params));
                    shpList.add(new Plane(params));

					System.out.println(String.format("Parsed plane (line %d)", lineNum));
				}
				else if (code.equals("elp"))
				{
					
					shpList.add(new Ellipsoid(params));

					System.out.println(String.format("Parsed ellipsoid (line %d)", lineNum));
				}
				else if (code.equals("lgt"))
				{
                    lgt.add(new Lights(params));

					System.out.println(String.format("Parsed light (line %d)", lineNum));
				}
				else
				{
					System.out.println(String.format("ERROR: Did not recognize object: %s (line %d)", code, lineNum));
				}
			}
		}

                // It is recommended that you check here that the scene is valid,
                // for example camera settings and all necessary materials were defined.

		System.out.println("Finished parsing scene file " + sceneFileName);

	}

	/**
	 * Renders the loaded scene and saves it to the specified file location.
	 */
	public void renderScene(String outputFileName)
	{
		long startTime = System.currentTimeMillis();
		
		//Initialize Ray Origin
		Ray ray = new Ray();
		ray.setP0(cam.getPosition());
		double[] clr;

		
		double[][] imgR = new double[imageWidth][imageWidth];
		double[][] imgG = new double[imageWidth][imageWidth];
		double[][] imgB = new double[imageWidth][imageWidth];
		
		for (int i = 0 ; i < imageWidth ; i++){
			for (int j = 0 ; j < imageHeight ; j++){
				
				
				//####Construct Ray Through Pixel: Here defines Ray.V
				//
				ray.constructRayThorughPixel(i, j, cam);
	
				//####Get Best HIT: Here defines Ray.T and Ray.BestShapeHit
				ray.getHit(shpList);
				
				//####Get Color of Best HIT
				clr = getColorFromHit(ray);

				imgR[i][j] = clr[0] * 255;
				imgG[i][j] = clr[1] * 255;
				imgB[i][j] = clr[2] * 255;
				
				
			}
		}
		
		
		// Create a byte array to hold the pixel data:
		byte[] rgbData = writeRGBToMat(imgR, imgG, imgB);
			


		long endTime = System.currentTimeMillis();
		Long renderTime = endTime - startTime;

                // The time is measured for your own conveniece, rendering speed will not affect your score
                // unless it is exceptionally slow (more than a couple of minutes)
		System.out.println("Finished rendering scene in " + renderTime.toString() + " milliseconds.");

                // This is already implemented, and should work without adding any code.
		saveImage(this.imageWidth, rgbData, outputFileName);

		System.out.println("Saved file " + outputFileName);

	}


	//////////////////////// FUNCTIONS TO SAVE IMAGES IN PNG FORMAT //////////////////////////////////////////


	
	public double[] getColorFromHit(Ray ray){
		
	if (ray.getT() < Double.POSITIVE_INFINITY){
		
			int mtlIndex = ray.getBestShape().getMtlIndex();
			Material currMtl = mtl.get(mtlIndex - 1);
			
			//Retrieves Shape Color
			return ray.getBestShape().getColor(currMtl ,ray, lgt);
			
		}  else {
			
			//Retrieves Background Color
			return set.getRGB();
		}
	}
	
	public static double[] GlobalGetColor(Material mtl, Ray r, ArrayList<Lights> lgtList, double[] N){
		
		//Initilise Vars
		double[] Kd =  mtl.getdRGB();
		double[] Ks = mtl.getsRGB();
		double[] Diff = new double[3];
		double[] Spec = new double[3];
		double phong = mtl.getPhong();
		double[] pointOnSurface = r.getP();	
		double[] V = r.getV();
		
		//Lightning Vals
		double[] lgtPos;
		
		double[] I;
		double SI;
		
		//DotProducts
		double dotProductLN;
		double dotProductVR;
		
		//Vectors
		double[] L;
		double[] R;
		
		//helpers
		double a;
		
		for (Lights lgt : lgtList) {
			
			//Get Lightning Data
			lgtPos =  lgt.getPosition();
			I = lgt.getlRGB();
			SI = lgt.getSpecularIntensity();
			
			// Vector L
			L = Vector.createVector(pointOnSurface, lgtPos);
			L = Vector.createNormalizedVectorv(L);
	

			// DOT LN
			dotProductLN = Math.max(Vector.computeDotProduct(N, L), 0);
			
			//Vector R
			a = Vector.computeDotProduct(Vector.multiplyVectorbyScalar(L, 2), N);
			R = Vector.computeVectorSubstraction( L, Vector.multiplyVectorbyScalar(N, a) );
			R = Vector.createNormalizedVectorv(R);	
			
			
			// DOT VR
			dotProductVR = Math.max(Vector.computeDotProduct(R, V), 0);
			
			//Diff Data
			for (int i = 0; i < 3; i ++)
				Diff[i] += Kd[i] * dotProductLN * I[i];
			
			//Spec Data
			for (int i = 0; i < 3; i ++)
				Spec[i] += SI * Ks[i] * Math.pow(dotProductVR,phong) * I[i];

			

		}

		
		double[] clr = Vector.computeVectorAddition(Diff, Spec);
		
		clr[0] = Math.max(0, Math.min(1, clr[0]));
		clr[1] = Math.max(0, Math.min(1, clr[1]));
		clr[2] = Math.max(0, Math.min(1, clr[2]));
	
		return clr;

	}
	
	



	public byte[] writeRGBToMat(double[][] imgR, double[][] imgG, double[][] imgB){
		// Create a byte array to hold the pixel data:
		byte[] rgbData = new byte[this.imageWidth * this.imageHeight * 3];
		for (int x = 0; x < this.imageWidth; x ++){
			for (int y = 0 ; y < this.imageHeight; y++){

				rgbData[(y * this.imageWidth + x) * 3] = (byte) imgR[x][y];
				rgbData[(y * this.imageWidth + x) * 3 + 1] = (byte) imgG[x][y];
				rgbData[(y * this.imageWidth + x) * 3 + 2] = (byte) imgB[x][y];
			}
		}
		return rgbData;
	}




	/*
	 * Saves RGB data as an image in png format to the specified location.
	 */
	public static void saveImage(int width, byte[] rgbData, String fileName)
	{
		try {

			BufferedImage image = bytes2RGB(width, rgbData);
			ImageIO.write(image, "png", new File(fileName));

		} catch (IOException e) {
			System.out.println("ERROR SAVING FILE: " + e.getMessage());
		}

	}

	/*
	 * Producing a BufferedImage that can be saved as png from a byte array of RGB values.
	 */
	public static BufferedImage bytes2RGB(int width, byte[] buffer) {
	    int height = buffer.length / width / 3;
	    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
	    ColorModel cm = new ComponentColorModel(cs, false, false,
	            Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
	    SampleModel sm = cm.createCompatibleSampleModel(width, height);
	    DataBufferByte db = new DataBufferByte(buffer, width * height);
	    WritableRaster raster = Raster.createWritableRaster(sm, db, null);
	    BufferedImage result = new BufferedImage(cm, raster, false, null);

	    return result;
	}

	public static class RayTracerException extends Exception {
		public RayTracerException(String msg) {  super(msg); }
	}


}
